feat!: drop rsipstack, run on in-house engine - #41
Closed
wavekat-eason wants to merge 13 commits into
Closed
Conversation
Add plan doc 08 for a clean-room SIP transaction/dialog/transport engine that replaces the external stack dependency. Kept as an internal pub(crate) `stack` module inside the single `wavekat-sip` crate (no second crate), keeping `rsip` for message types and migrating flow-by-flow behind a patched-fork bridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BBYdYGwVkEUhUfeAAUoA3p
First slice of the clean-room SIP engine planned in docs/08: the four RFC 3261 §17 transaction state machines (client/server INVITE and non-INVITE), written sans-IO so the timer-heavy core is unit-tested against the spec's timer tables with no sockets or wall-clock. Each machine maps an input event (received message, fired timer, or a TU response) to an ordered list of TxActions (send, arm/stop timer, deliver up, timed-out, terminated). Shared layer adds Timers (T1/T2/T4), Reliability, TransactionKey demux, branch generation, and the non-2xx ACK builder that reuses the INVITE's Via/branch/route set — structurally avoiding the re-INVITE ACK bug that motivated the rewrite. Entirely pub(crate): nothing appears in wavekat_sip::* signatures. 41 unit tests; fmt/clippy/test/doc all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 2 of the clean-room stack (docs/10): the transport layer and the async runner that drives the sans-IO §17 machines over a real socket. - transport.rs: rsip-backed (de)serialization + a bound tokio UDP socket that drops malformed datagrams instead of stalling. - engine.rs: one lock-free task owning the socket, transaction table and timers. Demuxes inbound by TransactionKey, opens server transactions, applies TxActions (send / arm-stop timer / publish Event), and routes an unmatched ACK up as the 2xx-ACK (dialog) concern. Timers use a generation counter instead of cancellable handles, so StopTimer and re-arm are just increments. - Transaction dispatch enum + Reliability::from(Transport). 8 new loopback-UDP tests with shrunk timers (49 stack tests total); fmt/clippy/test/doc all clean. Still entirely pub(crate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 3 of the clean-room stack (docs/11): src/stack/auth.rs. Reuses rsip's DigestGenerator for the HA1/HA2 math and owns only the orchestration: build_retry() reads a 401/407 challenge and produces the retried request — fresh Via branch, incremented CSeq, and the right Authorization / Proxy-Authorization header, with qop=auth cnonce/nc support. cnonce uses the same no-rand seeded-hash trick as branches. 7 unit tests including a DigestGenerator self-verify of the computed response (qop and no-qop). Known limitation documented: rsip spells the algorithm token "SHA256" not RFC 7616 "SHA-256"; MD5 is fully correct. Still entirely pub(crate). fmt/clippy/test/doc all clean (205 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 4 of the clean-room stack (docs/12): src/stack/dialog.rs. Dialog::uac / Dialog::uas build the caller/callee dialog state; new_request produces in-dialog requests (BYE/re-INVITE/INFO) with incremented CSeq, the remote target as Request-URI, correct From/To tags, a fresh Via branch, and the captured route set replayed as Route headers. DialogId (Call-ID + tags) routes inbound in-dialog requests. The motivating route-set bug is fixed by construction: the route set is captured once at establishment (UAC reversed, UAS in order) and replayed on every in-dialog request, never recomputed from a Contact. A regression test guards it. 5 unit tests; still entirely pub(crate). fmt/clippy/test/doc clean (210 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 5 of the clean-room stack (docs/13): src/stack/registration.rs. First composed flow — stitches the non-INVITE client transaction, the UDP engine, and the digest auth orchestration into a working REGISTER. build_register() composes the request; drive_register() sends it, answers one 401/407 via auth::build_retry, and reports a RegisterOutcome. Two of the three tests are full loopback round-trips against a fake registrar (real rsip-parsed SIP over real UDP): success after a digest challenge, and Unauthorized when credentials are always rejected. Still entirely pub(crate). fmt/clippy/test/doc clean (213 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 6 of the clean-room stack (docs/14): src/stack/call.rs. Second composed flow — place_call() sends an INVITE through the client INVITE transaction, follows provisional responses, answers one 401/407, and on a 2xx builds the dialog and sends the 2xx ACK out-of-dialog (reusing the INVITE CSeq per RFC 3261 §13.2.2.4). hangup() tears the call down with an in-dialog BYE. build_invite() carries the SDP offer. Two of three tests are full loopback round-trips against a fake callee: a complete INVITE→180→200→ACK→BYE lifecycle, and a 486 rejection where the transaction ACKs the non-2xx itself. Both UA signaling flows (REGISTER + INVITE) now run end-to-end on the clean-room engine. Still entirely pub(crate). fmt/clippy/test/doc clean (216 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Phase 7 toward dropping rsipstack: src/stack/ua.rs. The composed flows each assumed they owned the engine's single event stream. Ua owns that stream and routes each Event to the flow that owns its transaction (a TransactionKey -> Sender table, with a subscribe-ack handshake that closes the race against a fast reply); unmatched inbound requests (new INVITEs, the 2xx ACK) go to an `incoming` stream for the callee/dialog layer. Ua::register / call / hangup / answer / next_incoming are now the single canonical drivers, so the standalone drive_register/place_call/hangup are removed (registration.rs/call.rs keep the request builders + types). Two loopback tests prove a register + a call share one engine, and an inbound INVITE reaches `incoming` and is answered on the wire. This is the layer the public wrappers will sit on. Still pub(crate); public API unchanged. fmt/clippy/test/doc clean (216 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Adds src/stack/response.rs: build_response() composes a UAS response that echoes the request's Via/From/Call-ID/CSeq, copies To (adding a local tag when absent), and optionally carries a Contact and body. Needed by the callee (accept/reject) and the endpoint's in-dialog auto-answer in the upcoming public-wrapper cutover. Additive, pub(crate), 2 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Re-points every public wrapper onto the clean-room stack and removes the rsipstack dependency entirely. rsip (SIP message types) is the only SIP crate left; rsipstack is gone from Cargo.toml and the dependency tree. Public API (redesigned, engine-backed): - SipEndpoint::new -> Arc<Self>; owns the Ua (engine + router) and a background task that routes inbound requests: a new INVITE becomes an IncomingCall, in-dialog requests are auto-answered, the 2xx ACK is absorbed. next_incoming_call() yields inbound calls. - Caller::dial -> Call (binds RTP, offers SDP, answers a digest challenge, parses the SDP answer). - IncomingCall::accept -> Call / reject(status). - Call::hangup() sends an in-dialog BYE. - Registrar over Ua::register with keepalive + diagnostics. - re_exports now expose only rsip types. Deferred to follow-ups on the new API (modules removed for now): session timers (RFC 4028), DTMF INFO send, hold/resume re-INVITE, the DialogState stream / cancel-while-ringing UX, and the User-Agent header. The SDP/RTP layers and resolve are unchanged. fmt/clippy/test/doc all clean; rsipstack not in the dependency tree. BREAKING CHANGE: public types and signatures changed; the rsipstack re-exports are removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Adds tests/end_to_end_call.rs: drives the public API (SipEndpoint / Caller / IncomingCall / Call) over loopback on the in-house engine — one endpoint dials another, the callee accepts with an SDP answer, the caller confirms negotiated media (PCMU), then hangs up with a BYE the callee's router auto-answers. No external SIP server required. Docs: adds docs/16-drop-rsipstack.md (the cutover record, incl. deferred feature deltas) and indexes it; rewrites the living RFC-COVERAGE.md to match the engine-backed API (removes rsipstack framing and the dropped CANCEL / User-Agent / session-timer / INFO-DTMF rows; notes UDP-only transport and rport/TCP gaps). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
Drops an explicit intra-doc link target to the pub(crate) Ua type that rustdoc flagged as redundant, restoring a zero-warning cargo doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJyg8i9YjsgdEMp77PvRyq
This was referenced Jun 27, 2026
Contributor
Author
|
Folded into #43, which now targets |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the external
rsipstackdependency with a from-scratch, in-house SIP engine (built acrossdocs/08–docs/15) and re-points the entire public API onto it.rsipstackis removed fromCargo.tomland no longer appears in the dependency tree;rsip(SIP message types) is the only SIP crate left.This is the final phase (8) of the own-SIP-stack plan. See
docs/16-drop-rsipstack.mdfor the full record.New public API (engine-backed)
SipEndpoint::new → Arc<Self>— binds transport, starts the engine, resolves the next-hop server (RFC 3263 subset), and runs an inbound router: a freshINVITEbecomes anIncomingCall; in-dialogBYE/OPTIONS/INFO/re-INVITEare auto-answered200 OK; the 2xxACKis absorbed.Caller::dial → Call— binds RTP, offers G.711 SDP, answers one digest challenge, parses the SDP answer.IncomingCall::accept → Call/reject(status).Call::hangup— in-dialogBYE.RegistraroverUa::registerwith keepalive + preservedRegistrarDiagnostics.re_exportsnow exposes onlyrsiptypes.Tests
tests/end_to_end_call.rsdrives the public API over loopback on the in-house engine (dial → accept-with-SDP → confirm media → hangup), no external SIP server required.stack::ualoopback tests cover REGISTER-with-digest and the full INVITE/200/ACK/BYE flow.Gates
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace, andcargo doc --no-deps -p wavekat-sip --all-featuresall pass with zero warnings.rsipstackis absent fromCargo.lock.Public types and signatures changed and the rsipstack re-exports are removed. Consumers using
Caller::dial/PendingDial/Callee/DialogStatemust move to the newCall/IncomingCallshapes.Deferred (modules removed; reintroducible on the engine without further breaking changes)
User-AgentheaderTransport::Tcpis currently inert; engine is UDP-only)SDP, RTP, DTMF-over-RTP, and SRV resolution are unchanged.
🤖 Generated with Claude Code